# Zeroconf (mDNS) autodiscovery module for ParallelPython
# Copyright (c) 2007, Dieter Schuster
# All rights reserved.

# Redistribution and use in source and binary forms, with or without 
# modification, are permitted provided that the following conditions are met:
#    * Redistributions of source code must retain the above copyright notice, 
#      this list of conditions and the following disclaimer.
#    * Redistributions in binary form must reproduce the above copyright notice,
#      this list of conditions and the following disclaimer in the documentation 
#      and/or other materials provided with the distribution.
#    * Neither the name of the author nor the names of its contributors 
#      may be used to endorse or promote products derived from this software 
#      without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF 
# THE POSSIBILITY OF SUCH DAMAGE.
"""Parallel Python Software, Zeroconf autodiscovery

http://www.parallelpython.com - updates, documentation, examples and support forums
"""

import logging, socket, thread, random
import ppzeroconf

copyright = "Copyright (c) 2007 Dieter Schuster. All rights reserved"

class Discover:
    def __update_services(self, newattrs):
        """updates self.attrs, then revokes all announced services
        and publishes new ones, based on the new attrs"""
        self.attrs.update(newattrs)
        for ip,server in self.mdns_servers.items():
            for service in server.services.values():
                server.unregisterService(service)
            server.registerService(self.__build_service(ip)) 

    def __init__(self, base, isclient=False, auto_opts=None):
        self.base = base
        self.isclient = isclient
        self.mdns_servers = dict() # one for each local ip used
        self.mdns_browsers = dict()
        self.id = str(int(random.random()*(10**10))) # just a random 10 digit number
        self.attrs = {'group':'any'}
        if auto_opts:
            self.attrs.update(auto_opts)
        self.auto_opts = property(lambda: self.attrs, lambda x: self.__update_services(x))
    
    def __build_service(self, localip):
        if self.isclient:
            svc_name='_client'
            port = self.base.default_port
        else:
            svc_name='_server'
            port = self.base.port

        svc = ppzeroconf.ServiceInfo( svc_name + '._pp._tcp.local.',
                                    self.id + '.' + svc_name + '._pp._tcp.local.',
                                    socket.inet_aton(localip),
                                    port,
                                    0,
                                    0,
                                    self.attrs
                                    )
        logging.debug("mDNS: %s created" % (svc,))
        return svc

    def addService(self, server, type, name):
        """gets called, when the mDNS-Listener notices a new announced service"""
        info = server.getServiceInfo(type, name)
        attrs = info.getProperties()
        remote_address = (socket.inet_ntoa(info.getAddress()), info.getPort())
        id = "%s:%i" % remote_address
        logging.debug("mDNS: %s found" % (name,))
        if self.isclient:
            return
        else:
            if not self.base.autopp_list.get(id, 0) and "_client._pp" in name:
                if self.attrs['group']=='any' or attrs['group']==self.attrs['group']:
                    logging.debug("Connecting to host %s" % (id,))
                    thread.start_new_thread(self.base.connect1, remote_address+(False,))
                else:
                    logging.debug("Ignoring client belonging to group %s" % attrs['group'])

    def removeService(self, server, type, name):
        """gets called, when the mDNS-Listener recognizes, that a service is no longer available"""
        pass # doesn't do anything, yet

    def run(self, address):
        # get local ip based on given broadcast address:
        tempsock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        tempsock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
        tempsock.connect(address)
        localip,port = tempsock.getsockname()
        tempsock.close()

        if not localip in self.mdns_servers:
            logging.debug("starting zeroconf")
            server = ppzeroconf.Zeroconf(bindaddress=localip)
            logging.debug("binding zeroconf-service on %s" % (localip,))
            server.registerService(self.__build_service(localip))
            self.mdns_servers[localip] = server
            self.mdns_browsers[localip] = ppzeroconf.ServiceBrowser(server, "_pp._tcp.local.", self)
